/*
 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package com.sun.imageio.plugins.png;

import java.awt.image.ColorModel;
import java.awt.image.IndexColorModel;
import java.awt.image.SampleModel;
import java.util.ArrayList;
import java.util.StringTokenizer;
import javax.imageio.ImageTypeSpecifier;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.metadata.IIOMetadataFormatImpl;
import javax.imageio.metadata.IIOMetadataNode;
import org.w3c.dom.Node;

public class PNGMetadata extends IIOMetadata implements Cloneable {

  // package scope
  public static final String
      nativeMetadataFormatName = "javax_imageio_png_1.0";

  protected static final String nativeMetadataFormatClassName
      = "com.sun.imageio.plugins.png.PNGMetadataFormat";

  // Color types for IHDR chunk
  static final String[] IHDR_colorTypeNames = {
      "Grayscale", null, "RGB", "Palette",
      "GrayAlpha", null, "RGBAlpha"
  };

  static final int[] IHDR_numChannels = {
      1, 0, 3, 3, 2, 0, 4
  };

  // Bit depths for IHDR chunk
  static final String[] IHDR_bitDepths = {
      "1", "2", "4", "8", "16"
  };

  // Compression methods for IHDR chunk
  static final String[] IHDR_compressionMethodNames = {
      "deflate"
  };

  // Filter methods for IHDR chunk
  static final String[] IHDR_filterMethodNames = {
      "adaptive"
  };

  // Interlace methods for IHDR chunk
  static final String[] IHDR_interlaceMethodNames = {
      "none", "adam7"
  };

  // Compression methods for iCCP chunk
  static final String[] iCCP_compressionMethodNames = {
      "deflate"
  };

  // Compression methods for zTXt chunk
  static final String[] zTXt_compressionMethodNames = {
      "deflate"
  };

  // "Unknown" unit for pHYs chunk
  public static final int PHYS_UNIT_UNKNOWN = 0;

  // "Meter" unit for pHYs chunk
  public static final int PHYS_UNIT_METER = 1;

  // Unit specifiers for pHYs chunk
  static final String[] unitSpecifierNames = {
      "unknown", "meter"
  };

  // Rendering intents for sRGB chunk
  static final String[] renderingIntentNames = {
      "Perceptual", // 0
      "Relative colorimetric", // 1
      "Saturation", // 2
      "Absolute colorimetric" // 3

  };

  // Color space types for Chroma->ColorSpaceType node
  static final String[] colorSpaceTypeNames = {
      "GRAY", null, "RGB", "RGB",
      "GRAY", null, "RGB"
  };

  // IHDR chunk
  public boolean IHDR_present;
  public int IHDR_width;
  public int IHDR_height;
  public int IHDR_bitDepth;
  public int IHDR_colorType;
  public int IHDR_compressionMethod;
  public int IHDR_filterMethod;
  public int IHDR_interlaceMethod; // 0 == none, 1 == adam7

  // PLTE chunk
  public boolean PLTE_present;
  public byte[] PLTE_red;
  public byte[] PLTE_green;
  public byte[] PLTE_blue;

  // If non-null, used to reorder palette entries during encoding in
  // order to minimize the size of the tRNS chunk.  Thus an index of
  // 'i' in the source should be encoded as index 'PLTE_order[i]'.
  // PLTE_order will be null unless 'initialize' is called with an
  // IndexColorModel image type.
  public int[] PLTE_order = null;

  // bKGD chunk
  // If external (non-PNG sourced) data has red = green = blue,
  // always store it as gray and promote when writing
  public boolean bKGD_present;
  public int bKGD_colorType; // PNG_COLOR_GRAY, _RGB, or _PALETTE
  public int bKGD_index;
  public int bKGD_gray;
  public int bKGD_red;
  public int bKGD_green;
  public int bKGD_blue;

  // cHRM chunk
  public boolean cHRM_present;
  public int cHRM_whitePointX;
  public int cHRM_whitePointY;
  public int cHRM_redX;
  public int cHRM_redY;
  public int cHRM_greenX;
  public int cHRM_greenY;
  public int cHRM_blueX;
  public int cHRM_blueY;

  // gAMA chunk
  public boolean gAMA_present;
  public int gAMA_gamma;

  // hIST chunk
  public boolean hIST_present;
  public char[] hIST_histogram;

  // iCCP chunk
  public boolean iCCP_present;
  public String iCCP_profileName;
  public int iCCP_compressionMethod;
  public byte[] iCCP_compressedProfile;

  // iTXt chunk
  public ArrayList<String> iTXt_keyword = new ArrayList<String>();
  public ArrayList<Boolean> iTXt_compressionFlag = new ArrayList<Boolean>();
  public ArrayList<Integer> iTXt_compressionMethod = new ArrayList<Integer>();
  public ArrayList<String> iTXt_languageTag = new ArrayList<String>();
  public ArrayList<String> iTXt_translatedKeyword = new ArrayList<String>();
  public ArrayList<String> iTXt_text = new ArrayList<String>();

  // pHYs chunk
  public boolean pHYs_present;
  public int pHYs_pixelsPerUnitXAxis;
  public int pHYs_pixelsPerUnitYAxis;
  public int pHYs_unitSpecifier; // 0 == unknown, 1 == meter

  // sBIT chunk
  public boolean sBIT_present;
  public int sBIT_colorType; // PNG_COLOR_GRAY, _GRAY_ALPHA, _RGB, _RGB_ALPHA
  public int sBIT_grayBits;
  public int sBIT_redBits;
  public int sBIT_greenBits;
  public int sBIT_blueBits;
  public int sBIT_alphaBits;

  // sPLT chunk
  public boolean sPLT_present;
  public String sPLT_paletteName; // 1-79 characters
  public int sPLT_sampleDepth; // 8 or 16
  public int[] sPLT_red;
  public int[] sPLT_green;
  public int[] sPLT_blue;
  public int[] sPLT_alpha;
  public int[] sPLT_frequency;

  // sRGB chunk
  public boolean sRGB_present;
  public int sRGB_renderingIntent;

  // tEXt chunk
  public ArrayList<String> tEXt_keyword = new ArrayList<String>(); // 1-79 characters
  public ArrayList<String> tEXt_text = new ArrayList<String>();

  // tIME chunk
  public boolean tIME_present;
  public int tIME_year;
  public int tIME_month;
  public int tIME_day;
  public int tIME_hour;
  public int tIME_minute;
  public int tIME_second;

  // tRNS chunk
  // If external (non-PNG sourced) data has red = green = blue,
  // always store it as gray and promote when writing
  public boolean tRNS_present;
  public int tRNS_colorType; // PNG_COLOR_GRAY, _RGB, or _PALETTE
  public byte[] tRNS_alpha; // May have fewer entries than PLTE_red, etc.
  public int tRNS_gray;
  public int tRNS_red;
  public int tRNS_green;
  public int tRNS_blue;

  // zTXt chunk
  public ArrayList<String> zTXt_keyword = new ArrayList<String>();
  public ArrayList<Integer> zTXt_compressionMethod = new ArrayList<Integer>();
  public ArrayList<String> zTXt_text = new ArrayList<String>();

  // Unknown chunks
  public ArrayList<String> unknownChunkType = new ArrayList<String>();
  public ArrayList<byte[]> unknownChunkData = new ArrayList<byte[]>();

  public PNGMetadata() {
    super(true,
        nativeMetadataFormatName,
        nativeMetadataFormatClassName,
        null, null);
  }

  public PNGMetadata(IIOMetadata metadata) {
    // TODO -- implement
  }

  /**
   * Sets the IHDR_bitDepth and IHDR_colorType variables.
   * The <code>numBands</code> parameter is necessary since
   * we may only be writing a subset of the image bands.
   */
  public void initialize(ImageTypeSpecifier imageType, int numBands) {
    ColorModel colorModel = imageType.getColorModel();
    SampleModel sampleModel = imageType.getSampleModel();

    // Initialize IHDR_bitDepth
    int[] sampleSize = sampleModel.getSampleSize();
    int bitDepth = sampleSize[0];
    // Choose max bit depth over all channels
    // Fixes bug 4413109
    for (int i = 1; i < sampleSize.length; i++) {
      if (sampleSize[i] > bitDepth) {
        bitDepth = sampleSize[i];
      }
    }
    // Multi-channel images must have a bit depth of 8 or 16
    if (sampleSize.length > 1 && bitDepth < 8) {
      bitDepth = 8;
    }

    // Round bit depth up to a power of 2
    if (bitDepth > 2 && bitDepth < 4) {
      bitDepth = 4;
    } else if (bitDepth > 4 && bitDepth < 8) {
      bitDepth = 8;
    } else if (bitDepth > 8 && bitDepth < 16) {
      bitDepth = 16;
    } else if (bitDepth > 16) {
      throw new RuntimeException("bitDepth > 16!");
    }
    IHDR_bitDepth = bitDepth;

    // Initialize IHDR_colorType
    if (colorModel instanceof IndexColorModel) {
      IndexColorModel icm = (IndexColorModel) colorModel;
      int size = icm.getMapSize();

      byte[] reds = new byte[size];
      icm.getReds(reds);
      byte[] greens = new byte[size];
      icm.getGreens(greens);
      byte[] blues = new byte[size];
      icm.getBlues(blues);

      // Determine whether the color tables are actually a gray ramp
      // if the color type has not been set previously
      boolean isGray = false;
      if (!IHDR_present ||
          (IHDR_colorType != PNGImageReader.PNG_COLOR_PALETTE)) {
        isGray = true;
        int scale = 255 / ((1 << IHDR_bitDepth) - 1);
        for (int i = 0; i < size; i++) {
          byte red = reds[i];
          if ((red != (byte) (i * scale)) ||
              (red != greens[i]) ||
              (red != blues[i])) {
            isGray = false;
            break;
          }
        }
      }

      // Determine whether transparency exists
      boolean hasAlpha = colorModel.hasAlpha();

      byte[] alpha = null;
      if (hasAlpha) {
        alpha = new byte[size];
        icm.getAlphas(alpha);
      }

            /*
             * NB: PNG_COLOR_GRAY_ALPHA color type may be not optimal for images
             * contained more than 1024 pixels (or even than 768 pixels in case of
             * single transparent pixel in palette).
             * For such images alpha samples in raster will occupy more space than
             * it is required to store palette so it could be reasonable to
             * use PNG_COLOR_PALETTE color type for large images.
             */

      if (isGray && hasAlpha && (bitDepth == 8 || bitDepth == 16)) {
        IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY_ALPHA;
      } else if (isGray && !hasAlpha) {
        IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY;
      } else {
        IHDR_colorType = PNGImageReader.PNG_COLOR_PALETTE;
        PLTE_present = true;
        PLTE_order = null;
        PLTE_red = (byte[]) reds.clone();
        PLTE_green = (byte[]) greens.clone();
        PLTE_blue = (byte[]) blues.clone();

        if (hasAlpha) {
          tRNS_present = true;
          tRNS_colorType = PNGImageReader.PNG_COLOR_PALETTE;

          PLTE_order = new int[alpha.length];

          // Reorder the palette so that non-opaque entries
          // come first.  Since the tRNS chunk does not have
          // to store trailing 255's, this can save a
          // considerable amount of space when encoding
          // images with only one transparent pixel value,
          // e.g., images from GIF sources.

          byte[] newAlpha = new byte[alpha.length];

          // Scan for non-opaque entries and assign them
          // positions starting at 0.
          int newIndex = 0;
          for (int i = 0; i < alpha.length; i++) {
            if (alpha[i] != (byte) 255) {
              PLTE_order[i] = newIndex;
              newAlpha[newIndex] = alpha[i];
              ++newIndex;
            }
          }
          int numTransparent = newIndex;

          // Scan for opaque entries and assign them
          // positions following the non-opaque entries.
          for (int i = 0; i < alpha.length; i++) {
            if (alpha[i] == (byte) 255) {
              PLTE_order[i] = newIndex++;
            }
          }

          // Reorder the palettes
          byte[] oldRed = PLTE_red;
          byte[] oldGreen = PLTE_green;
          byte[] oldBlue = PLTE_blue;
          int len = oldRed.length; // All have the same length
          PLTE_red = new byte[len];
          PLTE_green = new byte[len];
          PLTE_blue = new byte[len];
          for (int i = 0; i < len; i++) {
            PLTE_red[PLTE_order[i]] = oldRed[i];
            PLTE_green[PLTE_order[i]] = oldGreen[i];
            PLTE_blue[PLTE_order[i]] = oldBlue[i];
          }

          // Copy only the transparent entries into tRNS_alpha
          tRNS_alpha = new byte[numTransparent];
          System.arraycopy(newAlpha, 0,
              tRNS_alpha, 0, numTransparent);
        }
      }
    } else {
      if (numBands == 1) {
        IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY;
      } else if (numBands == 2) {
        IHDR_colorType = PNGImageReader.PNG_COLOR_GRAY_ALPHA;
      } else if (numBands == 3) {
        IHDR_colorType = PNGImageReader.PNG_COLOR_RGB;
      } else if (numBands == 4) {
        IHDR_colorType = PNGImageReader.PNG_COLOR_RGB_ALPHA;
      } else {
        throw new RuntimeException("Number of bands not 1-4!");
      }
    }

    IHDR_present = true;
  }

  public boolean isReadOnly() {
    return false;
  }

  private ArrayList<byte[]> cloneBytesArrayList(ArrayList<byte[]> in) {
    if (in == null) {
      return null;
    } else {
      ArrayList<byte[]> list = new ArrayList<byte[]>(in.size());
      for (byte[] b : in) {
        list.add((b == null) ? null : (byte[]) b.clone());
      }
      return list;
    }
  }

  // Deep clone
  public Object clone() {
    PNGMetadata metadata;
    try {
      metadata = (PNGMetadata) super.clone();
    } catch (CloneNotSupportedException e) {
      return null;
    }

    // unknownChunkData needs deep clone
    metadata.unknownChunkData =
        cloneBytesArrayList(this.unknownChunkData);

    return metadata;
  }

  public Node getAsTree(String formatName) {
    if (formatName.equals(nativeMetadataFormatName)) {
      return getNativeTree();
    } else if (formatName.equals
        (IIOMetadataFormatImpl.standardMetadataFormatName)) {
      return getStandardTree();
    } else {
      throw new IllegalArgumentException("Not a recognized format!");
    }
  }

  private Node getNativeTree() {
    IIOMetadataNode node = null; // scratch node
    IIOMetadataNode root = new IIOMetadataNode(nativeMetadataFormatName);

    // IHDR
    if (IHDR_present) {
      IIOMetadataNode IHDR_node = new IIOMetadataNode("IHDR");
      IHDR_node.setAttribute("width", Integer.toString(IHDR_width));
      IHDR_node.setAttribute("height", Integer.toString(IHDR_height));
      IHDR_node.setAttribute("bitDepth",
          Integer.toString(IHDR_bitDepth));
      IHDR_node.setAttribute("colorType",
          IHDR_colorTypeNames[IHDR_colorType]);
      // IHDR_compressionMethod must be 0 in PNG 1.1
      IHDR_node.setAttribute("compressionMethod",
          IHDR_compressionMethodNames[IHDR_compressionMethod]);
      // IHDR_filterMethod must be 0 in PNG 1.1
      IHDR_node.setAttribute("filterMethod",
          IHDR_filterMethodNames[IHDR_filterMethod]);
      IHDR_node.setAttribute("interlaceMethod",
          IHDR_interlaceMethodNames[IHDR_interlaceMethod]);
      root.appendChild(IHDR_node);
    }

    // PLTE
    if (PLTE_present) {
      IIOMetadataNode PLTE_node = new IIOMetadataNode("PLTE");
      int numEntries = PLTE_red.length;
      for (int i = 0; i < numEntries; i++) {
        IIOMetadataNode entry = new IIOMetadataNode("PLTEEntry");
        entry.setAttribute("index", Integer.toString(i));
        entry.setAttribute("red",
            Integer.toString(PLTE_red[i] & 0xff));
        entry.setAttribute("green",
            Integer.toString(PLTE_green[i] & 0xff));
        entry.setAttribute("blue",
            Integer.toString(PLTE_blue[i] & 0xff));
        PLTE_node.appendChild(entry);
      }

      root.appendChild(PLTE_node);
    }

    // bKGD
    if (bKGD_present) {
      IIOMetadataNode bKGD_node = new IIOMetadataNode("bKGD");

      if (bKGD_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
        node = new IIOMetadataNode("bKGD_Palette");
        node.setAttribute("index", Integer.toString(bKGD_index));
      } else if (bKGD_colorType == PNGImageReader.PNG_COLOR_GRAY) {
        node = new IIOMetadataNode("bKGD_Grayscale");
        node.setAttribute("gray", Integer.toString(bKGD_gray));
      } else if (bKGD_colorType == PNGImageReader.PNG_COLOR_RGB) {
        node = new IIOMetadataNode("bKGD_RGB");
        node.setAttribute("red", Integer.toString(bKGD_red));
        node.setAttribute("green", Integer.toString(bKGD_green));
        node.setAttribute("blue", Integer.toString(bKGD_blue));
      }
      bKGD_node.appendChild(node);

      root.appendChild(bKGD_node);
    }

    // cHRM
    if (cHRM_present) {
      IIOMetadataNode cHRM_node = new IIOMetadataNode("cHRM");
      cHRM_node.setAttribute("whitePointX",
          Integer.toString(cHRM_whitePointX));
      cHRM_node.setAttribute("whitePointY",
          Integer.toString(cHRM_whitePointY));
      cHRM_node.setAttribute("redX", Integer.toString(cHRM_redX));
      cHRM_node.setAttribute("redY", Integer.toString(cHRM_redY));
      cHRM_node.setAttribute("greenX", Integer.toString(cHRM_greenX));
      cHRM_node.setAttribute("greenY", Integer.toString(cHRM_greenY));
      cHRM_node.setAttribute("blueX", Integer.toString(cHRM_blueX));
      cHRM_node.setAttribute("blueY", Integer.toString(cHRM_blueY));

      root.appendChild(cHRM_node);
    }

    // gAMA
    if (gAMA_present) {
      IIOMetadataNode gAMA_node = new IIOMetadataNode("gAMA");
      gAMA_node.setAttribute("value", Integer.toString(gAMA_gamma));

      root.appendChild(gAMA_node);
    }

    // hIST
    if (hIST_present) {
      IIOMetadataNode hIST_node = new IIOMetadataNode("hIST");

      for (int i = 0; i < hIST_histogram.length; i++) {
        IIOMetadataNode hist =
            new IIOMetadataNode("hISTEntry");
        hist.setAttribute("index", Integer.toString(i));
        hist.setAttribute("value",
            Integer.toString(hIST_histogram[i]));
        hIST_node.appendChild(hist);
      }

      root.appendChild(hIST_node);
    }

    // iCCP
    if (iCCP_present) {
      IIOMetadataNode iCCP_node = new IIOMetadataNode("iCCP");
      iCCP_node.setAttribute("profileName", iCCP_profileName);
      iCCP_node.setAttribute("compressionMethod",
          iCCP_compressionMethodNames[iCCP_compressionMethod]);

      Object profile = iCCP_compressedProfile;
      if (profile != null) {
        profile = ((byte[]) profile).clone();
      }
      iCCP_node.setUserObject(profile);

      root.appendChild(iCCP_node);
    }

    // iTXt
    if (iTXt_keyword.size() > 0) {
      IIOMetadataNode iTXt_parent = new IIOMetadataNode("iTXt");
      for (int i = 0; i < iTXt_keyword.size(); i++) {
        IIOMetadataNode iTXt_node = new IIOMetadataNode("iTXtEntry");
        iTXt_node.setAttribute("keyword", iTXt_keyword.get(i));
        iTXt_node.setAttribute("compressionFlag",
            iTXt_compressionFlag.get(i) ? "TRUE" : "FALSE");
        iTXt_node.setAttribute("compressionMethod",
            iTXt_compressionMethod.get(i).toString());
        iTXt_node.setAttribute("languageTag",
            iTXt_languageTag.get(i));
        iTXt_node.setAttribute("translatedKeyword",
            iTXt_translatedKeyword.get(i));
        iTXt_node.setAttribute("text", iTXt_text.get(i));

        iTXt_parent.appendChild(iTXt_node);
      }

      root.appendChild(iTXt_parent);
    }

    // pHYs
    if (pHYs_present) {
      IIOMetadataNode pHYs_node = new IIOMetadataNode("pHYs");
      pHYs_node.setAttribute("pixelsPerUnitXAxis",
          Integer.toString(pHYs_pixelsPerUnitXAxis));
      pHYs_node.setAttribute("pixelsPerUnitYAxis",
          Integer.toString(pHYs_pixelsPerUnitYAxis));
      pHYs_node.setAttribute("unitSpecifier",
          unitSpecifierNames[pHYs_unitSpecifier]);

      root.appendChild(pHYs_node);
    }

    // sBIT
    if (sBIT_present) {
      IIOMetadataNode sBIT_node = new IIOMetadataNode("sBIT");

      if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY) {
        node = new IIOMetadataNode("sBIT_Grayscale");
        node.setAttribute("gray",
            Integer.toString(sBIT_grayBits));
      } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
        node = new IIOMetadataNode("sBIT_GrayAlpha");
        node.setAttribute("gray",
            Integer.toString(sBIT_grayBits));
        node.setAttribute("alpha",
            Integer.toString(sBIT_alphaBits));
      } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_RGB) {
        node = new IIOMetadataNode("sBIT_RGB");
        node.setAttribute("red",
            Integer.toString(sBIT_redBits));
        node.setAttribute("green",
            Integer.toString(sBIT_greenBits));
        node.setAttribute("blue",
            Integer.toString(sBIT_blueBits));
      } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
        node = new IIOMetadataNode("sBIT_RGBAlpha");
        node.setAttribute("red",
            Integer.toString(sBIT_redBits));
        node.setAttribute("green",
            Integer.toString(sBIT_greenBits));
        node.setAttribute("blue",
            Integer.toString(sBIT_blueBits));
        node.setAttribute("alpha",
            Integer.toString(sBIT_alphaBits));
      } else if (sBIT_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
        node = new IIOMetadataNode("sBIT_Palette");
        node.setAttribute("red",
            Integer.toString(sBIT_redBits));
        node.setAttribute("green",
            Integer.toString(sBIT_greenBits));
        node.setAttribute("blue",
            Integer.toString(sBIT_blueBits));
      }
      sBIT_node.appendChild(node);

      root.appendChild(sBIT_node);
    }

    // sPLT
    if (sPLT_present) {
      IIOMetadataNode sPLT_node = new IIOMetadataNode("sPLT");

      sPLT_node.setAttribute("name", sPLT_paletteName);
      sPLT_node.setAttribute("sampleDepth",
          Integer.toString(sPLT_sampleDepth));

      int numEntries = sPLT_red.length;
      for (int i = 0; i < numEntries; i++) {
        IIOMetadataNode entry = new IIOMetadataNode("sPLTEntry");
        entry.setAttribute("index", Integer.toString(i));
        entry.setAttribute("red", Integer.toString(sPLT_red[i]));
        entry.setAttribute("green", Integer.toString(sPLT_green[i]));
        entry.setAttribute("blue", Integer.toString(sPLT_blue[i]));
        entry.setAttribute("alpha", Integer.toString(sPLT_alpha[i]));
        entry.setAttribute("frequency",
            Integer.toString(sPLT_frequency[i]));
        sPLT_node.appendChild(entry);
      }

      root.appendChild(sPLT_node);
    }

    // sRGB
    if (sRGB_present) {
      IIOMetadataNode sRGB_node = new IIOMetadataNode("sRGB");
      sRGB_node.setAttribute("renderingIntent",
          renderingIntentNames[sRGB_renderingIntent]);

      root.appendChild(sRGB_node);
    }

    // tEXt
    if (tEXt_keyword.size() > 0) {
      IIOMetadataNode tEXt_parent = new IIOMetadataNode("tEXt");
      for (int i = 0; i < tEXt_keyword.size(); i++) {
        IIOMetadataNode tEXt_node = new IIOMetadataNode("tEXtEntry");
        tEXt_node.setAttribute("keyword", (String) tEXt_keyword.get(i));
        tEXt_node.setAttribute("value", (String) tEXt_text.get(i));

        tEXt_parent.appendChild(tEXt_node);
      }

      root.appendChild(tEXt_parent);
    }

    // tIME
    if (tIME_present) {
      IIOMetadataNode tIME_node = new IIOMetadataNode("tIME");
      tIME_node.setAttribute("year", Integer.toString(tIME_year));
      tIME_node.setAttribute("month", Integer.toString(tIME_month));
      tIME_node.setAttribute("day", Integer.toString(tIME_day));
      tIME_node.setAttribute("hour", Integer.toString(tIME_hour));
      tIME_node.setAttribute("minute", Integer.toString(tIME_minute));
      tIME_node.setAttribute("second", Integer.toString(tIME_second));

      root.appendChild(tIME_node);
    }

    // tRNS
    if (tRNS_present) {
      IIOMetadataNode tRNS_node = new IIOMetadataNode("tRNS");

      if (tRNS_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
        node = new IIOMetadataNode("tRNS_Palette");

        for (int i = 0; i < tRNS_alpha.length; i++) {
          IIOMetadataNode entry =
              new IIOMetadataNode("tRNS_PaletteEntry");
          entry.setAttribute("index", Integer.toString(i));
          entry.setAttribute("alpha",
              Integer.toString(tRNS_alpha[i] & 0xff));
          node.appendChild(entry);
        }
      } else if (tRNS_colorType == PNGImageReader.PNG_COLOR_GRAY) {
        node = new IIOMetadataNode("tRNS_Grayscale");
        node.setAttribute("gray", Integer.toString(tRNS_gray));
      } else if (tRNS_colorType == PNGImageReader.PNG_COLOR_RGB) {
        node = new IIOMetadataNode("tRNS_RGB");
        node.setAttribute("red", Integer.toString(tRNS_red));
        node.setAttribute("green", Integer.toString(tRNS_green));
        node.setAttribute("blue", Integer.toString(tRNS_blue));
      }
      tRNS_node.appendChild(node);

      root.appendChild(tRNS_node);
    }

    // zTXt
    if (zTXt_keyword.size() > 0) {
      IIOMetadataNode zTXt_parent = new IIOMetadataNode("zTXt");
      for (int i = 0; i < zTXt_keyword.size(); i++) {
        IIOMetadataNode zTXt_node = new IIOMetadataNode("zTXtEntry");
        zTXt_node.setAttribute("keyword", (String) zTXt_keyword.get(i));

        int cm = ((Integer) zTXt_compressionMethod.get(i)).intValue();
        zTXt_node.setAttribute("compressionMethod",
            zTXt_compressionMethodNames[cm]);

        zTXt_node.setAttribute("text", (String) zTXt_text.get(i));

        zTXt_parent.appendChild(zTXt_node);
      }

      root.appendChild(zTXt_parent);
    }

    // Unknown chunks
    if (unknownChunkType.size() > 0) {
      IIOMetadataNode unknown_parent =
          new IIOMetadataNode("UnknownChunks");
      for (int i = 0; i < unknownChunkType.size(); i++) {
        IIOMetadataNode unknown_node =
            new IIOMetadataNode("UnknownChunk");
        unknown_node.setAttribute("type",
            (String) unknownChunkType.get(i));
        unknown_node.setUserObject((byte[]) unknownChunkData.get(i));

        unknown_parent.appendChild(unknown_node);
      }

      root.appendChild(unknown_parent);
    }

    return root;
  }

  private int getNumChannels() {
    // Determine number of channels
    // Be careful about palette color with transparency
    int numChannels = IHDR_numChannels[IHDR_colorType];
    if (IHDR_colorType == PNGImageReader.PNG_COLOR_PALETTE &&
        tRNS_present && tRNS_colorType == IHDR_colorType) {
      numChannels = 4;
    }
    return numChannels;
  }

  public IIOMetadataNode getStandardChromaNode() {
    IIOMetadataNode chroma_node = new IIOMetadataNode("Chroma");
    IIOMetadataNode node = null; // scratch node

    node = new IIOMetadataNode("ColorSpaceType");
    node.setAttribute("name", colorSpaceTypeNames[IHDR_colorType]);
    chroma_node.appendChild(node);

    node = new IIOMetadataNode("NumChannels");
    node.setAttribute("value", Integer.toString(getNumChannels()));
    chroma_node.appendChild(node);

    if (gAMA_present) {
      node = new IIOMetadataNode("Gamma");
      node.setAttribute("value", Float.toString(gAMA_gamma * 1.0e-5F));
      chroma_node.appendChild(node);
    }

    node = new IIOMetadataNode("BlackIsZero");
    node.setAttribute("value", "TRUE");
    chroma_node.appendChild(node);

    if (PLTE_present) {
      boolean hasAlpha = tRNS_present &&
          (tRNS_colorType == PNGImageReader.PNG_COLOR_PALETTE);

      node = new IIOMetadataNode("Palette");
      for (int i = 0; i < PLTE_red.length; i++) {
        IIOMetadataNode entry =
            new IIOMetadataNode("PaletteEntry");
        entry.setAttribute("index", Integer.toString(i));
        entry.setAttribute("red",
            Integer.toString(PLTE_red[i] & 0xff));
        entry.setAttribute("green",
            Integer.toString(PLTE_green[i] & 0xff));
        entry.setAttribute("blue",
            Integer.toString(PLTE_blue[i] & 0xff));
        if (hasAlpha) {
          int alpha = (i < tRNS_alpha.length) ?
              (tRNS_alpha[i] & 0xff) : 255;
          entry.setAttribute("alpha", Integer.toString(alpha));
        }
        node.appendChild(entry);
      }
      chroma_node.appendChild(node);
    }

    if (bKGD_present) {
      if (bKGD_colorType == PNGImageReader.PNG_COLOR_PALETTE) {
        node = new IIOMetadataNode("BackgroundIndex");
        node.setAttribute("value", Integer.toString(bKGD_index));
      } else {
        node = new IIOMetadataNode("BackgroundColor");
        int r, g, b;

        if (bKGD_colorType == PNGImageReader.PNG_COLOR_GRAY) {
          r = g = b = bKGD_gray;
        } else {
          r = bKGD_red;
          g = bKGD_green;
          b = bKGD_blue;
        }
        node.setAttribute("red", Integer.toString(r));
        node.setAttribute("green", Integer.toString(g));
        node.setAttribute("blue", Integer.toString(b));
      }
      chroma_node.appendChild(node);
    }

    return chroma_node;
  }

  public IIOMetadataNode getStandardCompressionNode() {
    IIOMetadataNode compression_node = new IIOMetadataNode("Compression");
    IIOMetadataNode node = null; // scratch node

    node = new IIOMetadataNode("CompressionTypeName");
    node.setAttribute("value", "deflate");
    compression_node.appendChild(node);

    node = new IIOMetadataNode("Lossless");
    node.setAttribute("value", "TRUE");
    compression_node.appendChild(node);

    node = new IIOMetadataNode("NumProgressiveScans");
    node.setAttribute("value",
        (IHDR_interlaceMethod == 0) ? "1" : "7");
    compression_node.appendChild(node);

    return compression_node;
  }

  private String repeat(String s, int times) {
    if (times == 1) {
      return s;
    }
    StringBuffer sb = new StringBuffer((s.length() + 1) * times - 1);
    sb.append(s);
    for (int i = 1; i < times; i++) {
      sb.append(" ");
      sb.append(s);
    }
    return sb.toString();
  }

  public IIOMetadataNode getStandardDataNode() {
    IIOMetadataNode data_node = new IIOMetadataNode("Data");
    IIOMetadataNode node = null; // scratch node

    node = new IIOMetadataNode("PlanarConfiguration");
    node.setAttribute("value", "PixelInterleaved");
    data_node.appendChild(node);

    node = new IIOMetadataNode("SampleFormat");
    node.setAttribute("value",
        IHDR_colorType == PNGImageReader.PNG_COLOR_PALETTE ?
            "Index" : "UnsignedIntegral");
    data_node.appendChild(node);

    String bitDepth = Integer.toString(IHDR_bitDepth);
    node = new IIOMetadataNode("BitsPerSample");
    node.setAttribute("value", repeat(bitDepth, getNumChannels()));
    data_node.appendChild(node);

    if (sBIT_present) {
      node = new IIOMetadataNode("SignificantBitsPerSample");
      String sbits;
      if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY ||
          sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) {
        sbits = Integer.toString(sBIT_grayBits);
      } else { // sBIT_colorType == PNGImageReader.PNG_COLOR_RGB ||
        // sBIT_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA
        sbits = Integer.toString(sBIT_redBits) + " " +
            Integer.toString(sBIT_greenBits) + " " +
            Integer.toString(sBIT_blueBits);
      }

      if (sBIT_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA ||
          sBIT_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) {
        sbits += " " + Integer.toString(sBIT_alphaBits);
      }

      node.setAttribute("value", sbits);
      data_node.appendChild(node);
    }

    // SampleMSB

    return data_node;
  }

  public IIOMetadataNode getStandardDimensionNode() {
    IIOMetadataNode dimension_node = new IIOMetadataNode("Dimension");
    IIOMetadataNode node = null; // scratch node

    node = new IIOMetadataNode("PixelAspectRatio");
    float ratio = pHYs_present ?
        (float) pHYs_pixelsPerUnitXAxis / pHYs_pixelsPerUnitYAxis : 1.0F;
    node.setAttribute("value", Float.toString(ratio));
    dimension_node.appendChild(node);

    node = new IIOMetadataNode("ImageOrientation");
    node.setAttribute("value", "Normal");
    dimension_node.appendChild(node);

    if (pHYs_present && pHYs_unitSpecifier == PHYS_UNIT_METER) {
      node = new IIOMetadataNode("HorizontalPixelSize");
      node.setAttribute("value",
          Float.toString(1000.0F / pHYs_pixelsPerUnitXAxis));
      dimension_node.appendChild(node);

      node = new IIOMetadataNode("VerticalPixelSize");
      node.setAttribute("value",
          Float.toString(1000.0F / pHYs_pixelsPerUnitYAxis));
      dimension_node.appendChild(node);
    }

    return dimension_node;
  }

  public IIOMetadataNode getStandardDocumentNode() {
    if (!tIME_present) {
      return null;
    }

    IIOMetadataNode document_node = new IIOMetadataNode("Document");
    IIOMetadataNode node = null; // scratch node

    node = new IIOMetadataNode("ImageModificationTime");
    node.setAttribute("year", Integer.toString(tIME_year));
    node.setAttribute("month", Integer.toString(tIME_month));
    node.setAttribute("day", Integer.toString(tIME_day));
    node.setAttribute("hour", Integer.toString(tIME_hour));
    node.setAttribute("minute", Integer.toString(tIME_minute));
    node.setAttribute("second", Integer.toString(tIME_second));
    document_node.appendChild(node);

    return document_node;
  }

  public IIOMetadataNode getStandardTextNode() {
    int numEntries = tEXt_keyword.size() +
        iTXt_keyword.size() + zTXt_keyword.size();
    if (numEntries == 0) {
      return null;
    }

    IIOMetadataNode text_node = new IIOMetadataNode("Text");
    IIOMetadataNode node = null; // scratch node

    for (int i = 0; i < tEXt_keyword.size(); i++) {
      node = new IIOMetadataNode("TextEntry");
      node.setAttribute("keyword", (String) tEXt_keyword.get(i));
      node.setAttribute("value", (String) tEXt_text.get(i));
      node.setAttribute("encoding", "ISO-8859-1");
      node.setAttribute("compression", "none");

      text_node.appendChild(node);
    }

    for (int i = 0; i < iTXt_keyword.size(); i++) {
      node = new IIOMetadataNode("TextEntry");
      node.setAttribute("keyword", iTXt_keyword.get(i));
      node.setAttribute("value", iTXt_text.get(i));
      node.setAttribute("language",
          iTXt_languageTag.get(i));
      if (iTXt_compressionFlag.get(i)) {
        node.setAttribute("compression", "zip");
      } else {
        node.setAttribute("compression", "none");
      }

      text_node.appendChild(node);
    }

    for (int i = 0; i < zTXt_keyword.size(); i++) {
      node = new IIOMetadataNode("TextEntry");
      node.setAttribute("keyword", (String) zTXt_keyword.get(i));
      node.setAttribute("value", (String) zTXt_text.get(i));
      node.setAttribute("compression", "zip");

      text_node.appendChild(node);
    }

    return text_node;
  }

  public IIOMetadataNode getStandardTransparencyNode() {
    IIOMetadataNode transparency_node =
        new IIOMetadataNode("Transparency");
    IIOMetadataNode node = null; // scratch node

    node = new IIOMetadataNode("Alpha");
    boolean hasAlpha =
        (IHDR_colorType == PNGImageReader.PNG_COLOR_RGB_ALPHA) ||
            (IHDR_colorType == PNGImageReader.PNG_COLOR_GRAY_ALPHA) ||
            (IHDR_colorType == PNGImageReader.PNG_COLOR_PALETTE &&
                tRNS_present &&
                (tRNS_colorType == IHDR_colorType) &&
                (tRNS_alpha != null));
    node.setAttribute("value", hasAlpha ? "nonpremultipled" : "none");
    transparency_node.appendChild(node);

    if (tRNS_present) {
      node = new IIOMetadataNode("TransparentColor");
      if (tRNS_colorType == PNGImageReader.PNG_COLOR_RGB) {
        node.setAttribute("value",
            Integer.toString(tRNS_red) + " " +
                Integer.toString(tRNS_green) + " " +
                Integer.toString(tRNS_blue));
      } else if (tRNS_colorType == PNGImageReader.PNG_COLOR_GRAY) {
        node.setAttribute("value", Integer.toString(tRNS_gray));
      }
      transparency_node.appendChild(node);
    }

    return transparency_node;
  }

  // Shorthand for throwing an IIOInvalidTreeException
  private void fatal(Node node, String reason)
      throws IIOInvalidTreeException {
    throw new IIOInvalidTreeException(reason, node);
  }

  // Get an integer-valued attribute
  private String getStringAttribute(Node node, String name,
      String defaultValue, boolean required)
      throws IIOInvalidTreeException {
    Node attr = node.getAttributes().getNamedItem(name);
    if (attr == null) {
      if (!required) {
        return defaultValue;
      } else {
        fatal(node, "Required attribute " + name + " not present!");
      }
    }
    return attr.getNodeValue();
  }


  // Get an integer-valued attribute
  private int getIntAttribute(Node node, String name,
      int defaultValue, boolean required)
      throws IIOInvalidTreeException {
    String value = getStringAttribute(node, name, null, required);
    if (value == null) {
      return defaultValue;
    }
    return Integer.parseInt(value);
  }

  // Get a float-valued attribute
  private float getFloatAttribute(Node node, String name,
      float defaultValue, boolean required)
      throws IIOInvalidTreeException {
    String value = getStringAttribute(node, name, null, required);
    if (value == null) {
      return defaultValue;
    }
    return Float.parseFloat(value);
  }

  // Get a required integer-valued attribute
  private int getIntAttribute(Node node, String name)
      throws IIOInvalidTreeException {
    return getIntAttribute(node, name, -1, true);
  }

  // Get a required float-valued attribute
  private float getFloatAttribute(Node node, String name)
      throws IIOInvalidTreeException {
    return getFloatAttribute(node, name, -1.0F, true);
  }

  // Get a boolean-valued attribute
  private boolean getBooleanAttribute(Node node, String name,
      boolean defaultValue,
      boolean required)
      throws IIOInvalidTreeException {
    Node attr = node.getAttributes().getNamedItem(name);
    if (attr == null) {
      if (!required) {
        return defaultValue;
      } else {
        fatal(node, "Required attribute " + name + " not present!");
      }
    }
    String value = attr.getNodeValue();
    // Allow lower case booleans for backward compatibility, #5082756
    if (value.equals("TRUE") || value.equals("true")) {
      return true;
    } else if (value.equals("FALSE") || value.equals("false")) {
      return false;
    } else {
      fatal(node, "Attribute " + name + " must be 'TRUE' or 'FALSE'!");
      return false;
    }
  }

  // Get a required boolean-valued attribute
  private boolean getBooleanAttribute(Node node, String name)
      throws IIOInvalidTreeException {
    return getBooleanAttribute(node, name, false, true);
  }

  // Get an enumerated attribute as an index into a String array
  private int getEnumeratedAttribute(Node node,
      String name, String[] legalNames,
      int defaultValue, boolean required)
      throws IIOInvalidTreeException {
    Node attr = node.getAttributes().getNamedItem(name);
    if (attr == null) {
      if (!required) {
        return defaultValue;
      } else {
        fatal(node, "Required attribute " + name + " not present!");
      }
    }
    String value = attr.getNodeValue();
    for (int i = 0; i < legalNames.length; i++) {
      if (value.equals(legalNames[i])) {
        return i;
      }
    }

    fatal(node, "Illegal value for attribute " + name + "!");
    return -1;
  }

  // Get a required enumerated attribute as an index into a String array
  private int getEnumeratedAttribute(Node node,
      String name, String[] legalNames)
      throws IIOInvalidTreeException {
    return getEnumeratedAttribute(node, name, legalNames, -1, true);
  }

  // Get a String-valued attribute
  private String getAttribute(Node node, String name,
      String defaultValue, boolean required)
      throws IIOInvalidTreeException {
    Node attr = node.getAttributes().getNamedItem(name);
    if (attr == null) {
      if (!required) {
        return defaultValue;
      } else {
        fatal(node, "Required attribute " + name + " not present!");
      }
    }
    return attr.getNodeValue();
  }

  // Get a required String-valued attribute
  private String getAttribute(Node node, String name)
      throws IIOInvalidTreeException {
    return getAttribute(node, name, null, true);
  }

  public void mergeTree(String formatName, Node root)
      throws IIOInvalidTreeException {
    if (formatName.equals(nativeMetadataFormatName)) {
      if (root == null) {
        throw new IllegalArgumentException("root == null!");
      }
      mergeNativeTree(root);
    } else if (formatName.equals
        (IIOMetadataFormatImpl.standardMetadataFormatName)) {
      if (root == null) {
        throw new IllegalArgumentException("root == null!");
      }
      mergeStandardTree(root);
    } else {
      throw new IllegalArgumentException("Not a recognized format!");
    }
  }

  private void mergeNativeTree(Node root)
      throws IIOInvalidTreeException {
    Node node = root;
    if (!node.getNodeName().equals(nativeMetadataFormatName)) {
      fatal(node, "Root must be " + nativeMetadataFormatName);
    }

    node = node.getFirstChild();
    while (node != null) {
      String name = node.getNodeName();

      if (name.equals("IHDR")) {
        IHDR_width = getIntAttribute(node, "width");
        IHDR_height = getIntAttribute(node, "height");
        IHDR_bitDepth =
            Integer.valueOf(IHDR_bitDepths[
                getEnumeratedAttribute(node,
                    "bitDepth",
                    IHDR_bitDepths)]);
        IHDR_colorType = getEnumeratedAttribute(node, "colorType",
            IHDR_colorTypeNames);
        IHDR_compressionMethod =
            getEnumeratedAttribute(node, "compressionMethod",
                IHDR_compressionMethodNames);
        IHDR_filterMethod =
            getEnumeratedAttribute(node,
                "filterMethod",
                IHDR_filterMethodNames);
        IHDR_interlaceMethod =
            getEnumeratedAttribute(node, "interlaceMethod",
                IHDR_interlaceMethodNames);
        IHDR_present = true;
      } else if (name.equals("PLTE")) {
        byte[] red = new byte[256];
        byte[] green = new byte[256];
        byte[] blue = new byte[256];
        int maxindex = -1;

        Node PLTE_entry = node.getFirstChild();
        if (PLTE_entry == null) {
          fatal(node, "Palette has no entries!");
        }

        while (PLTE_entry != null) {
          if (!PLTE_entry.getNodeName().equals("PLTEEntry")) {
            fatal(node,
                "Only a PLTEEntry may be a child of a PLTE!");
          }

          int index = getIntAttribute(PLTE_entry, "index");
          if (index < 0 || index > 255) {
            fatal(node,
                "Bad value for PLTEEntry attribute index!");
          }
          if (index > maxindex) {
            maxindex = index;
          }
          red[index] =
              (byte) getIntAttribute(PLTE_entry, "red");
          green[index] =
              (byte) getIntAttribute(PLTE_entry, "green");
          blue[index] =
              (byte) getIntAttribute(PLTE_entry, "blue");

          PLTE_entry = PLTE_entry.getNextSibling();
        }

        int numEntries = maxindex + 1;
        PLTE_red = new byte[numEntries];
        PLTE_green = new byte[numEntries];
        PLTE_blue = new byte[numEntries];
        System.arraycopy(red, 0, PLTE_red, 0, numEntries);
        System.arraycopy(green, 0, PLTE_green, 0, numEntries);
        System.arraycopy(blue, 0, PLTE_blue, 0, numEntries);
        PLTE_present = true;
      } else if (name.equals("bKGD")) {
        bKGD_present = false; // Guard against partial overwrite
        Node bKGD_node = node.getFirstChild();
        if (bKGD_node == null) {
          fatal(node, "bKGD node has no children!");
        }
        String bKGD_name = bKGD_node.getNodeName();
        if (bKGD_name.equals("bKGD_Palette")) {
          bKGD_index = getIntAttribute(bKGD_node, "index");
          bKGD_colorType = PNGImageReader.PNG_COLOR_PALETTE;
        } else if (bKGD_name.equals("bKGD_Grayscale")) {
          bKGD_gray = getIntAttribute(bKGD_node, "gray");
          bKGD_colorType = PNGImageReader.PNG_COLOR_GRAY;
        } else if (bKGD_name.equals("bKGD_RGB")) {
          bKGD_red = getIntAttribute(bKGD_node, "red");
          bKGD_green = getIntAttribute(bKGD_node, "green");
          bKGD_blue = getIntAttribute(bKGD_node, "blue");
          bKGD_colorType = PNGImageReader.PNG_COLOR_RGB;
        } else {
          fatal(node, "Bad child of a bKGD node!");
        }
        if (bKGD_node.getNextSibling() != null) {
          fatal(node, "bKGD node has more than one child!");
        }

        bKGD_present = true;
      } else if (name.equals("cHRM")) {
        cHRM_whitePointX = getIntAttribute(node, "whitePointX");
        cHRM_whitePointY = getIntAttribute(node, "whitePointY");
        cHRM_redX = getIntAttribute(node, "redX");
        cHRM_redY = getIntAttribute(node, "redY");
        cHRM_greenX = getIntAttribute(node, "greenX");
        cHRM_greenY = getIntAttribute(node, "greenY");
        cHRM_blueX = getIntAttribute(node, "blueX");
        cHRM_blueY = getIntAttribute(node, "blueY");

        cHRM_present = true;
      } else if (name.equals("gAMA")) {
        gAMA_gamma = getIntAttribute(node, "value");
        gAMA_present = true;
      } else if (name.equals("hIST")) {
        char[] hist = new char[256];
        int maxindex = -1;

        Node hIST_entry = node.getFirstChild();
        if (hIST_entry == null) {
          fatal(node, "hIST node has no children!");
        }

        while (hIST_entry != null) {
          if (!hIST_entry.getNodeName().equals("hISTEntry")) {
            fatal(node,
                "Only a hISTEntry may be a child of a hIST!");
          }

          int index = getIntAttribute(hIST_entry, "index");
          if (index < 0 || index > 255) {
            fatal(node,
                "Bad value for histEntry attribute index!");
          }
          if (index > maxindex) {
            maxindex = index;
          }
          hist[index] =
              (char) getIntAttribute(hIST_entry, "value");

          hIST_entry = hIST_entry.getNextSibling();
        }

        int numEntries = maxindex + 1;
        hIST_histogram = new char[numEntries];
        System.arraycopy(hist, 0, hIST_histogram, 0, numEntries);

        hIST_present = true;
      } else if (name.equals("iCCP")) {
        iCCP_profileName = getAttribute(node, "profileName");
        iCCP_compressionMethod =
            getEnumeratedAttribute(node, "compressionMethod",
                iCCP_compressionMethodNames);
        Object compressedProfile =
            ((IIOMetadataNode) node).getUserObject();
        if (compressedProfile == null) {
          fatal(node, "No ICCP profile present in user object!");
        }
        if (!(compressedProfile instanceof byte[])) {
          fatal(node, "User object not a byte array!");
        }

        iCCP_compressedProfile =
            (byte[]) ((byte[]) compressedProfile).clone();

        iCCP_present = true;
      } else if (name.equals("iTXt")) {
        Node iTXt_node = node.getFirstChild();
        while (iTXt_node != null) {
          if (!iTXt_node.getNodeName().equals("iTXtEntry")) {
            fatal(node,
                "Only an iTXtEntry may be a child of an iTXt!");
          }

          String keyword = getAttribute(iTXt_node, "keyword");
          if (isValidKeyword(keyword)) {
            iTXt_keyword.add(keyword);

            boolean compressionFlag =
                getBooleanAttribute(iTXt_node, "compressionFlag");
            iTXt_compressionFlag.add(Boolean.valueOf(compressionFlag));

            String compressionMethod =
                getAttribute(iTXt_node, "compressionMethod");
            iTXt_compressionMethod.add(Integer.valueOf(compressionMethod));

            String languageTag =
                getAttribute(iTXt_node, "languageTag");
            iTXt_languageTag.add(languageTag);

            String translatedKeyword =
                getAttribute(iTXt_node, "translatedKeyword");
            iTXt_translatedKeyword.add(translatedKeyword);

            String text = getAttribute(iTXt_node, "text");
            iTXt_text.add(text);

          }
          // silently skip invalid text entry

          iTXt_node = iTXt_node.getNextSibling();
        }
      } else if (name.equals("pHYs")) {
        pHYs_pixelsPerUnitXAxis =
            getIntAttribute(node, "pixelsPerUnitXAxis");
        pHYs_pixelsPerUnitYAxis =
            getIntAttribute(node, "pixelsPerUnitYAxis");
        pHYs_unitSpecifier =
            getEnumeratedAttribute(node, "unitSpecifier",
                unitSpecifierNames);

        pHYs_present = true;
      } else if (name.equals("sBIT")) {
        sBIT_present = false; // Guard against partial overwrite
        Node sBIT_node = node.getFirstChild();
        if (sBIT_node == null) {
          fatal(node, "sBIT node has no children!");
        }
        String sBIT_name = sBIT_node.getNodeName();
        if (sBIT_name.equals("sBIT_Grayscale")) {
          sBIT_grayBits = getIntAttribute(sBIT_node, "gray");
          sBIT_colorType = PNGImageReader.PNG_COLOR_GRAY;
        } else if (sBIT_name.equals("sBIT_GrayAlpha")) {
          sBIT_grayBits = getIntAttribute(sBIT_node, "gray");
          sBIT_alphaBits = getIntAttribute(sBIT_node, "alpha");
          sBIT_colorType = PNGImageReader.PNG_COLOR_GRAY_ALPHA;
        } else if (sBIT_name.equals("sBIT_RGB")) {
          sBIT_redBits = getIntAttribute(sBIT_node, "red");
          sBIT_greenBits = getIntAttribute(sBIT_node, "green");
          sBIT_blueBits = getIntAttribute(sBIT_node, "blue");
          sBIT_colorType = PNGImageReader.PNG_COLOR_RGB;
        } else if (sBIT_name.equals("sBIT_RGBAlpha")) {
          sBIT_redBits = getIntAttribute(sBIT_node, "red");
          sBIT_greenBits = getIntAttribute(sBIT_node, "green");
          sBIT_blueBits = getIntAttribute(sBIT_node, "blue");
          sBIT_alphaBits = getIntAttribute(sBIT_node, "alpha");
          sBIT_colorType = PNGImageReader.PNG_COLOR_RGB_ALPHA;
        } else if (sBIT_name.equals("sBIT_Palette")) {
          sBIT_redBits = getIntAttribute(sBIT_node, "red");
          sBIT_greenBits = getIntAttribute(sBIT_node, "green");
          sBIT_blueBits = getIntAttribute(sBIT_node, "blue");
          sBIT_colorType = PNGImageReader.PNG_COLOR_PALETTE;
        } else {
          fatal(node, "Bad child of an sBIT node!");
        }
        if (sBIT_node.getNextSibling() != null) {
          fatal(node, "sBIT node has more than one child!");
        }

        sBIT_present = true;
      } else if (name.equals("sPLT")) {
        sPLT_paletteName = getAttribute(node, "name");
        sPLT_sampleDepth = getIntAttribute(node, "sampleDepth");

        int[] red = new int[256];
        int[] green = new int[256];
        int[] blue = new int[256];
        int[] alpha = new int[256];
        int[] frequency = new int[256];
        int maxindex = -1;

        Node sPLT_entry = node.getFirstChild();
        if (sPLT_entry == null) {
          fatal(node, "sPLT node has no children!");
        }

        while (sPLT_entry != null) {
          if (!sPLT_entry.getNodeName().equals("sPLTEntry")) {
            fatal(node,
                "Only an sPLTEntry may be a child of an sPLT!");
          }

          int index = getIntAttribute(sPLT_entry, "index");
          if (index < 0 || index > 255) {
            fatal(node,
                "Bad value for PLTEEntry attribute index!");
          }
          if (index > maxindex) {
            maxindex = index;
          }
          red[index] = getIntAttribute(sPLT_entry, "red");
          green[index] = getIntAttribute(sPLT_entry, "green");
          blue[index] = getIntAttribute(sPLT_entry, "blue");
          alpha[index] = getIntAttribute(sPLT_entry, "alpha");
          frequency[index] =
              getIntAttribute(sPLT_entry, "frequency");

          sPLT_entry = sPLT_entry.getNextSibling();
        }

        int numEntries = maxindex + 1;
        sPLT_red = new int[numEntries];
        sPLT_green = new int[numEntries];
        sPLT_blue = new int[numEntries];
        sPLT_alpha = new int[numEntries];
        sPLT_frequency = new int[numEntries];
        System.arraycopy(red, 0, sPLT_red, 0, numEntries);
        System.arraycopy(green, 0, sPLT_green, 0, numEntries);
        System.arraycopy(blue, 0, sPLT_blue, 0, numEntries);
        System.arraycopy(alpha, 0, sPLT_alpha, 0, numEntries);
        System.arraycopy(frequency, 0,
            sPLT_frequency, 0, numEntries);

        sPLT_present = true;
      } else if (name.equals("sRGB")) {
        sRGB_renderingIntent =
            getEnumeratedAttribute(node, "renderingIntent",
                renderingIntentNames);

        sRGB_present = true;
      } else if (name.equals("tEXt")) {
        Node tEXt_node = node.getFirstChild();
        while (tEXt_node != null) {
          if (!tEXt_node.getNodeName().equals("tEXtEntry")) {
            fatal(node,
                "Only an tEXtEntry may be a child of an tEXt!");
          }

          String keyword = getAttribute(tEXt_node, "keyword");
          tEXt_keyword.add(keyword);

          String text = getAttribute(tEXt_node, "value");
          tEXt_text.add(text);

          tEXt_node = tEXt_node.getNextSibling();
        }
      } else if (name.equals("tIME")) {
        tIME_year = getIntAttribute(node, "year");
        tIME_month = getIntAttribute(node, "month");
        tIME_day = getIntAttribute(node, "day");
        tIME_hour = getIntAttribute(node, "hour");
        tIME_minute = getIntAttribute(node, "minute");
        tIME_second = getIntAttribute(node, "second");

        tIME_present = true;
      } else if (name.equals("tRNS")) {
        tRNS_present = false; // Guard against partial overwrite
        Node tRNS_node = node.getFirstChild();
        if (tRNS_node == null) {
          fatal(node, "tRNS node has no children!");
        }
        String tRNS_name = tRNS_node.getNodeName();
        if (tRNS_name.equals("tRNS_Palette")) {
          byte[] alpha = new byte[256];
          int maxindex = -1;

          Node tRNS_paletteEntry = tRNS_node.getFirstChild();
          if (tRNS_paletteEntry == null) {
            fatal(node, "tRNS_Palette node has no children!");
          }
          while (tRNS_paletteEntry != null) {
            if (!tRNS_paletteEntry.getNodeName().equals(
                "tRNS_PaletteEntry")) {
              fatal(node,
                  "Only a tRNS_PaletteEntry may be a child of a tRNS_Palette!");
            }
            int index =
                getIntAttribute(tRNS_paletteEntry, "index");
            if (index < 0 || index > 255) {
              fatal(node,
                  "Bad value for tRNS_PaletteEntry attribute index!");
            }
            if (index > maxindex) {
              maxindex = index;
            }
            alpha[index] =
                (byte) getIntAttribute(tRNS_paletteEntry,
                    "alpha");

            tRNS_paletteEntry =
                tRNS_paletteEntry.getNextSibling();
          }

          int numEntries = maxindex + 1;
          tRNS_alpha = new byte[numEntries];
          tRNS_colorType = PNGImageReader.PNG_COLOR_PALETTE;
          System.arraycopy(alpha, 0, tRNS_alpha, 0, numEntries);
        } else if (tRNS_name.equals("tRNS_Grayscale")) {
          tRNS_gray = getIntAttribute(tRNS_node, "gray");
          tRNS_colorType = PNGImageReader.PNG_COLOR_GRAY;
        } else if (tRNS_name.equals("tRNS_RGB")) {
          tRNS_red = getIntAttribute(tRNS_node, "red");
          tRNS_green = getIntAttribute(tRNS_node, "green");
          tRNS_blue = getIntAttribute(tRNS_node, "blue");
          tRNS_colorType = PNGImageReader.PNG_COLOR_RGB;
        } else {
          fatal(node, "Bad child of a tRNS node!");
        }
        if (tRNS_node.getNextSibling() != null) {
          fatal(node, "tRNS node has more than one child!");
        }

        tRNS_present = true;
      } else if (name.equals("zTXt")) {
        Node zTXt_node = node.getFirstChild();
        while (zTXt_node != null) {
          if (!zTXt_node.getNodeName().equals("zTXtEntry")) {
            fatal(node,
                "Only an zTXtEntry may be a child of an zTXt!");
          }

          String keyword = getAttribute(zTXt_node, "keyword");
          zTXt_keyword.add(keyword);

          int compressionMethod =
              getEnumeratedAttribute(zTXt_node, "compressionMethod",
                  zTXt_compressionMethodNames);
          zTXt_compressionMethod.add(new Integer(compressionMethod));

          String text = getAttribute(zTXt_node, "text");
          zTXt_text.add(text);

          zTXt_node = zTXt_node.getNextSibling();
        }
      } else if (name.equals("UnknownChunks")) {
        Node unknown_node = node.getFirstChild();
        while (unknown_node != null) {
          if (!unknown_node.getNodeName().equals("UnknownChunk")) {
            fatal(node,
                "Only an UnknownChunk may be a child of an UnknownChunks!");
          }
          String chunkType = getAttribute(unknown_node, "type");
          Object chunkData =
              ((IIOMetadataNode) unknown_node).getUserObject();

          if (chunkType.length() != 4) {
            fatal(unknown_node,
                "Chunk type must be 4 characters!");
          }
          if (chunkData == null) {
            fatal(unknown_node,
                "No chunk data present in user object!");
          }
          if (!(chunkData instanceof byte[])) {
            fatal(unknown_node,
                "User object not a byte array!");
          }
          unknownChunkType.add(chunkType);
          unknownChunkData.add(((byte[]) chunkData).clone());

          unknown_node = unknown_node.getNextSibling();
        }
      } else {
        fatal(node, "Unknown child of root node!");
      }

      node = node.getNextSibling();
    }
  }

  /*
   * Accrding to PNG spec, keywords are restricted to 1 to 79 bytes
   * in length. Keywords shall contain only printable Latin-1 characters
   * and spaces; To reduce the chances for human misreading of a keyword,
   * leading spaces, trailing spaces, and consecutive spaces are not
   * permitted in keywords.
   *
   * See: http://www.w3.org/TR/PNG/#11keywords
   */
  private boolean isValidKeyword(String s) {
    int len = s.length();
    if (len < 1 || len >= 80) {
      return false;
    }
    if (s.startsWith(" ") || s.endsWith(" ") || s.contains("  ")) {
      return false;
    }
    return isISOLatin(s, false);
  }

  /*
   * According to PNG spec, keyword shall contain only printable
   * Latin-1 [ISO-8859-1] characters and spaces; that is, only
   * character codes 32-126 and 161-255 decimal are allowed.
   * For Latin-1 value fields the 0x10 (linefeed) control
   * character is aloowed too.
   *
   * See: http://www.w3.org/TR/PNG/#11keywords
   */
  private boolean isISOLatin(String s, boolean isLineFeedAllowed) {
    int len = s.length();
    for (int i = 0; i < len; i++) {
      char c = s.charAt(i);
      if (c < 32 || c > 255 || (c > 126 && c < 161)) {
        // not printable. Check whether this is an allowed
        // control char
        if (!isLineFeedAllowed || c != 0x10) {
          return false;
        }
      }
    }
    return true;
  }

  private void mergeStandardTree(Node root)
      throws IIOInvalidTreeException {
    Node node = root;
    if (!node.getNodeName()
        .equals(IIOMetadataFormatImpl.standardMetadataFormatName)) {
      fatal(node, "Root must be " +
          IIOMetadataFormatImpl.standardMetadataFormatName);
    }

    node = node.getFirstChild();
    while (node != null) {
      String name = node.getNodeName();

      if (name.equals("Chroma")) {
        Node child = node.getFirstChild();
        while (child != null) {
          String childName = child.getNodeName();
          if (childName.equals("Gamma")) {
            float gamma = getFloatAttribute(child, "value");
            gAMA_present = true;
            gAMA_gamma = (int) (gamma * 100000 + 0.5);
          } else if (childName.equals("Palette")) {
            byte[] red = new byte[256];
            byte[] green = new byte[256];
            byte[] blue = new byte[256];
            int maxindex = -1;

            Node entry = child.getFirstChild();
            while (entry != null) {
              int index = getIntAttribute(entry, "index");
              if (index >= 0 && index <= 255) {
                red[index] =
                    (byte) getIntAttribute(entry, "red");
                green[index] =
                    (byte) getIntAttribute(entry, "green");
                blue[index] =
                    (byte) getIntAttribute(entry, "blue");
                if (index > maxindex) {
                  maxindex = index;
                }
              }
              entry = entry.getNextSibling();
            }

            int numEntries = maxindex + 1;
            PLTE_red = new byte[numEntries];
            PLTE_green = new byte[numEntries];
            PLTE_blue = new byte[numEntries];
            System.arraycopy(red, 0, PLTE_red, 0, numEntries);
            System.arraycopy(green, 0, PLTE_green, 0, numEntries);
            System.arraycopy(blue, 0, PLTE_blue, 0, numEntries);
            PLTE_present = true;
          } else if (childName.equals("BackgroundIndex")) {
            bKGD_present = true;
            bKGD_colorType = PNGImageReader.PNG_COLOR_PALETTE;
            bKGD_index = getIntAttribute(child, "value");
          } else if (childName.equals("BackgroundColor")) {
            int red = getIntAttribute(child, "red");
            int green = getIntAttribute(child, "green");
            int blue = getIntAttribute(child, "blue");
            if (red == green && red == blue) {
              bKGD_colorType = PNGImageReader.PNG_COLOR_GRAY;
              bKGD_gray = red;
            } else {
              bKGD_red = red;
              bKGD_green = green;
              bKGD_blue = blue;
            }
            bKGD_present = true;
          }
//                  } else if (childName.equals("ColorSpaceType")) {
//                  } else if (childName.equals("NumChannels")) {

          child = child.getNextSibling();
        }
      } else if (name.equals("Compression")) {
        Node child = node.getFirstChild();
        while (child != null) {
          String childName = child.getNodeName();
          if (childName.equals("NumProgressiveScans")) {
            // Use Adam7 if NumProgressiveScans > 1
            int scans = getIntAttribute(child, "value");
            IHDR_interlaceMethod = (scans > 1) ? 1 : 0;
//                  } else if (childName.equals("CompressionTypeName")) {
//                  } else if (childName.equals("Lossless")) {
//                  } else if (childName.equals("BitRate")) {
          }
          child = child.getNextSibling();
        }
      } else if (name.equals("Data")) {
        Node child = node.getFirstChild();
        while (child != null) {
          String childName = child.getNodeName();
          if (childName.equals("BitsPerSample")) {
            String s = getAttribute(child, "value");
            StringTokenizer t = new StringTokenizer(s);
            int maxBits = -1;
            while (t.hasMoreTokens()) {
              int bits = Integer.parseInt(t.nextToken());
              if (bits > maxBits) {
                maxBits = bits;
              }
            }
            if (maxBits < 1) {
              maxBits = 1;
            }
            if (maxBits == 3) {
              maxBits = 4;
            }
            if (maxBits > 4 || maxBits < 8) {
              maxBits = 8;
            }
            if (maxBits > 8) {
              maxBits = 16;
            }
            IHDR_bitDepth = maxBits;
          } else if (childName.equals("SignificantBitsPerSample")) {
            String s = getAttribute(child, "value");
            StringTokenizer t = new StringTokenizer(s);
            int numTokens = t.countTokens();
            if (numTokens == 1) {
              sBIT_colorType = PNGImageReader.PNG_COLOR_GRAY;
              sBIT_grayBits = Integer.parseInt(t.nextToken());
            } else if (numTokens == 2) {
              sBIT_colorType =
                  PNGImageReader.PNG_COLOR_GRAY_ALPHA;
              sBIT_grayBits = Integer.parseInt(t.nextToken());
              sBIT_alphaBits = Integer.parseInt(t.nextToken());
            } else if (numTokens == 3) {
              sBIT_colorType = PNGImageReader.PNG_COLOR_RGB;
              sBIT_redBits = Integer.parseInt(t.nextToken());
              sBIT_greenBits = Integer.parseInt(t.nextToken());
              sBIT_blueBits = Integer.parseInt(t.nextToken());
            } else if (numTokens == 4) {
              sBIT_colorType =
                  PNGImageReader.PNG_COLOR_RGB_ALPHA;
              sBIT_redBits = Integer.parseInt(t.nextToken());
              sBIT_greenBits = Integer.parseInt(t.nextToken());
              sBIT_blueBits = Integer.parseInt(t.nextToken());
              sBIT_alphaBits = Integer.parseInt(t.nextToken());
            }
            if (numTokens >= 1 && numTokens <= 4) {
              sBIT_present = true;
            }
//                      } else if (childName.equals("PlanarConfiguration")) {
//                      } else if (childName.equals("SampleFormat")) {
//                      } else if (childName.equals("SampleMSB")) {
          }
          child = child.getNextSibling();
        }
      } else if (name.equals("Dimension")) {
        boolean gotWidth = false;
        boolean gotHeight = false;
        boolean gotAspectRatio = false;

        float width = -1.0F;
        float height = -1.0F;
        float aspectRatio = -1.0F;

        Node child = node.getFirstChild();
        while (child != null) {
          String childName = child.getNodeName();
          if (childName.equals("PixelAspectRatio")) {
            aspectRatio = getFloatAttribute(child, "value");
            gotAspectRatio = true;
          } else if (childName.equals("HorizontalPixelSize")) {
            width = getFloatAttribute(child, "value");
            gotWidth = true;
          } else if (childName.equals("VerticalPixelSize")) {
            height = getFloatAttribute(child, "value");
            gotHeight = true;
//                  } else if (childName.equals("ImageOrientation")) {
//                  } else if
//                      (childName.equals("HorizontalPhysicalPixelSpacing")) {
//                  } else if
//                      (childName.equals("VerticalPhysicalPixelSpacing")) {
//                  } else if (childName.equals("HorizontalPosition")) {
//                  } else if (childName.equals("VerticalPosition")) {
//                  } else if (childName.equals("HorizontalPixelOffset")) {
//                  } else if (childName.equals("VerticalPixelOffset")) {
          }
          child = child.getNextSibling();
        }

        if (gotWidth && gotHeight) {
          pHYs_present = true;
          pHYs_unitSpecifier = 1;
          pHYs_pixelsPerUnitXAxis = (int) (width * 1000 + 0.5F);
          pHYs_pixelsPerUnitYAxis = (int) (height * 1000 + 0.5F);
        } else if (gotAspectRatio) {
          pHYs_present = true;
          pHYs_unitSpecifier = 0;

          // Find a reasonable rational approximation
          int denom = 1;
          for (; denom < 100; denom++) {
            int num = (int) (aspectRatio * denom);
            if (Math.abs(num / denom - aspectRatio) < 0.001) {
              break;
            }
          }
          pHYs_pixelsPerUnitXAxis = (int) (aspectRatio * denom);
          pHYs_pixelsPerUnitYAxis = denom;
        }
      } else if (name.equals("Document")) {
        Node child = node.getFirstChild();
        while (child != null) {
          String childName = child.getNodeName();
          if (childName.equals("ImageModificationTime")) {
            tIME_present = true;
            tIME_year = getIntAttribute(child, "year");
            tIME_month = getIntAttribute(child, "month");
            tIME_day = getIntAttribute(child, "day");
            tIME_hour =
                getIntAttribute(child, "hour", 0, false);
            tIME_minute =
                getIntAttribute(child, "minute", 0, false);
            tIME_second =
                getIntAttribute(child, "second", 0, false);
//                  } else if (childName.equals("SubimageInterpretation")) {
//                  } else if (childName.equals("ImageCreationTime")) {
          }
          child = child.getNextSibling();
        }
      } else if (name.equals("Text")) {
        Node child = node.getFirstChild();
        while (child != null) {
          String childName = child.getNodeName();
          if (childName.equals("TextEntry")) {
            String keyword =
                getAttribute(child, "keyword", "", false);
            String value = getAttribute(child, "value");
            String language =
                getAttribute(child, "language", "", false);
            String compression =
                getAttribute(child, "compression", "none", false);

            if (!isValidKeyword(keyword)) {
              // Just ignore this node, PNG requires keywords
            } else if (isISOLatin(value, true)) {
              if (compression.equals("zip")) {
                // Use a zTXt node
                zTXt_keyword.add(keyword);
                zTXt_text.add(value);
                zTXt_compressionMethod.add(Integer.valueOf(0));
              } else {
                // Use a tEXt node
                tEXt_keyword.add(keyword);
                tEXt_text.add(value);
              }
            } else {
              // Use an iTXt node
              iTXt_keyword.add(keyword);
              iTXt_compressionFlag.add(Boolean.valueOf(compression.equals("zip")));
              iTXt_compressionMethod.add(Integer.valueOf(0));
              iTXt_languageTag.add(language);
              iTXt_translatedKeyword.add(keyword); // fake it
              iTXt_text.add(value);
            }
          }
          child = child.getNextSibling();
        }
//          } else if (name.equals("Transparency")) {
//              Node child = node.getFirstChild();
//              while (child != null) {
//                  String childName = child.getNodeName();
//                  if (childName.equals("Alpha")) {
//                  } else if (childName.equals("TransparentIndex")) {
//                  } else if (childName.equals("TransparentColor")) {
//                  } else if (childName.equals("TileTransparencies")) {
//                  } else if (childName.equals("TileOpacities")) {
//                  }
//                  child = child.getNextSibling();
//              }
//          } else {
//              // fatal(node, "Unknown child of root node!");
      }

      node = node.getNextSibling();
    }
  }

  // Reset all instance variables to their initial state
  public void reset() {
    IHDR_present = false;
    PLTE_present = false;
    bKGD_present = false;
    cHRM_present = false;
    gAMA_present = false;
    hIST_present = false;
    iCCP_present = false;
    iTXt_keyword = new ArrayList<String>();
    iTXt_compressionFlag = new ArrayList<Boolean>();
    iTXt_compressionMethod = new ArrayList<Integer>();
    iTXt_languageTag = new ArrayList<String>();
    iTXt_translatedKeyword = new ArrayList<String>();
    iTXt_text = new ArrayList<String>();
    pHYs_present = false;
    sBIT_present = false;
    sPLT_present = false;
    sRGB_present = false;
    tEXt_keyword = new ArrayList<String>();
    tEXt_text = new ArrayList<String>();
    tIME_present = false;
    tRNS_present = false;
    zTXt_keyword = new ArrayList<String>();
    zTXt_compressionMethod = new ArrayList<Integer>();
    zTXt_text = new ArrayList<String>();
    unknownChunkType = new ArrayList<String>();
    unknownChunkData = new ArrayList<byte[]>();
  }
}
